using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using PaintIsland;

public class World : MonoBehaviour
{
    public const float CHUNK_LOAD_RADIUS = 3;
    public const float CHUNK_UNLOAD_RADIUS = 4;


    public Transform playerTransform;

    public Chunk chunkPrefab;

    public int chunkCount;

    // Chunks loaded.
    private List<Chunk> chunks;

    private Vector3 prevPlayerChunkPos;

    private void Awake()
    {
        chunks = new List<Chunk>();
    }

    void Start()
    {
        var playerChunkPos = World2ChunkPos(playerTransform.position);
        AddChunk(Instantiate(chunkPrefab, Vector3.zero, Quaternion.identity, transform));

        for (int x = 3; x < 12; x++)
        {
            for (int y = 3; y < 12; y++)
            {
                for (int z = 3; z < 12; z++)
                {
                    if (y == 11) SetBlock(BlockID.Grass, x, y, z);
                    else SetBlock(BlockID.Dirt, x, y, z);
                }
            }
        }

        SetBlock(BlockID.Bedrock, 8, 8, 8);


        prevPlayerChunkPos = playerChunkPos;
    }

    // Update is called once per frame
    void Update()
    {
        var playerChunkPos = World2ChunkPos(playerTransform.position);
        if (!Mathf.Approximately(Vector3.Distance(prevPlayerChunkPos, playerChunkPos), 0))
        {
            // Check chunks
            //Debug.Log($"Player entered new Chunk! {playerChunkPos}");

            RefreshChunks(playerChunkPos);
            chunkCount = chunks.Count;
            

            //Debug.Log($"Chunks: {transform.childCount}");

            prevPlayerChunkPos = playerChunkPos;
        }
    }

    // Called when player entered new chunk.
    // This method will load new near chunks and unload far chunks.
    private void RefreshChunks(Vector3 playerChunkPos)
    {
        // if player's position has chunk, do refresh.
        if (GetChunkAt(playerChunkPos) is Chunk c)
        {
            //Debug.Log($"Refresh kernel: {c.transform.position}");

            // Uses BFS algorithm to check if a chunk needs to load/unload.
            foreach (var chunk in chunks)
            {
                chunk.deprecated = true;
            }
            c.deprecated = false;

            // BFS initialization.
            var bfsQueue = new Queue<(Chunk, int)>();
            bfsQueue.Enqueue((c, 0));

            // BFS loop.
            while (bfsQueue.Count != 0)
            {
                var (chunk, step) = bfsQueue.Dequeue();

                if (step >= CHUNK_UNLOAD_RADIUS)
                {
                    chunk.deprecated = true;
                    continue;
                }
                
                // Traverse left chunk.
                if (chunk.neighbours[ChunkDirection.Left] is Chunk left && left.deprecated && step < CHUNK_UNLOAD_RADIUS)
                {
                    left.deprecated = false;
                    bfsQueue.Enqueue((left, step + 1));
                }
                else if (chunk.neighbours[ChunkDirection.Left] == null && step < CHUNK_LOAD_RADIUS)
                {
                    Chunk newChunk = Instantiate(chunkPrefab, chunk.transform.position + Chunk.CHUNK_SIZE * Vector3.left, Quaternion.identity, transform);
                    newChunk.deprecated = false;
                    if (Level.GetInstance().GetChunkData(newChunk.transform.position) is BlockID[,,] data)
                    {
                        newChunk.LoadData(data);
                    }
                    AddChunk(newChunk);
                    bfsQueue.Enqueue((newChunk, step + 1));
                }

                // Traverse right chunk.
                if (chunk.neighbours[ChunkDirection.Right] is Chunk right && right.deprecated && step < CHUNK_UNLOAD_RADIUS)
                {
                    right.deprecated = false;
                    bfsQueue.Enqueue((right, step + 1));
                }
                else if (chunk.neighbours[ChunkDirection.Right] == null && step < CHUNK_LOAD_RADIUS)
                {
                    Chunk newChunk = Instantiate(chunkPrefab, chunk.transform.position + Chunk.CHUNK_SIZE * Vector3.right, Quaternion.identity, transform);
                    newChunk.deprecated = false;
                    if (Level.GetInstance().GetChunkData(newChunk.transform.position) is BlockID[,,] data)
                    {
                        newChunk.LoadData(data);
                    }
                    AddChunk(newChunk);
                    bfsQueue.Enqueue((newChunk, step + 1));
                }

                // Traverse front chunk.
                if (chunk.neighbours[ChunkDirection.Far] is Chunk far && far.deprecated && step < CHUNK_UNLOAD_RADIUS)
                {
                    far.deprecated = false;
                    bfsQueue.Enqueue((far, step + 1));
                }
                else if (chunk.neighbours[ChunkDirection.Far] == null && step < CHUNK_LOAD_RADIUS)
                {
                    Chunk newChunk = Instantiate(chunkPrefab, chunk.transform.position + Chunk.CHUNK_SIZE * Vector3.forward, Quaternion.identity, transform);
                    newChunk.deprecated = false;
                    if (Level.GetInstance().GetChunkData(newChunk.transform.position) is BlockID[,,] data)
                    {
                        newChunk.LoadData(data);
                    }
                    AddChunk(newChunk);
                    bfsQueue.Enqueue((newChunk, step + 1));
                }

                // Traverse back chunk.
                if (chunk.neighbours[ChunkDirection.Near] is Chunk near && near.deprecated && step < CHUNK_UNLOAD_RADIUS)
                {
                    near.deprecated = false;
                    bfsQueue.Enqueue((near, step + 1));
                }
                else if (chunk.neighbours[ChunkDirection.Near] == null && step < CHUNK_LOAD_RADIUS)
                {
                    Chunk newChunk = Instantiate(chunkPrefab, chunk.transform.position + Chunk.CHUNK_SIZE * Vector3.back, Quaternion.identity, transform);
                    newChunk.deprecated = false;
                    if (Level.GetInstance().GetChunkData(newChunk.transform.position) is BlockID[,,] data)
                    {
                        newChunk.LoadData(data);
                    }
                    AddChunk(newChunk);
                    bfsQueue.Enqueue((newChunk, step + 1));
                }
                
                // Traverse top chunk.
                if (chunk.neighbours[ChunkDirection.Top] is Chunk top && top.deprecated && step < CHUNK_UNLOAD_RADIUS)
                {
                    top.deprecated = false;
                    bfsQueue.Enqueue((top, step + 1));
                }
                else if (chunk.neighbours[ChunkDirection.Top] == null && step < CHUNK_LOAD_RADIUS)
                {
                    Chunk newChunk = Instantiate(chunkPrefab, chunk.transform.position + Chunk.CHUNK_SIZE * Vector3.up, Quaternion.identity, transform);
                    newChunk.deprecated = false;
                    if (Level.GetInstance().GetChunkData(newChunk.transform.position) is BlockID[,,] data)
                    {
                        newChunk.LoadData(data);
                    }
                    AddChunk(newChunk);
                    bfsQueue.Enqueue((newChunk, step + 1));
                }

                if (chunk.neighbours[ChunkDirection.Bottom] is Chunk bottom && bottom.deprecated && step < CHUNK_UNLOAD_RADIUS)
                {
                    bottom.deprecated = false;
                    bfsQueue.Enqueue((bottom, step + 1));
                }
                else if (chunk.neighbours[ChunkDirection.Bottom] == null && step < CHUNK_LOAD_RADIUS)
                {
                    Chunk newChunk = Instantiate(chunkPrefab, chunk.transform.position + Chunk.CHUNK_SIZE * Vector3.down, Quaternion.identity, transform);
                    newChunk.deprecated = false;
                    if (Level.GetInstance().GetChunkData(newChunk.transform.position) is BlockID[,,] data)
                    {
                        newChunk.LoadData(data);
                    }
                    AddChunk(newChunk);
                    bfsQueue.Enqueue((newChunk, step + 1));
                }
            }

            var deprecatedChunks = (from chunk in chunks
                       where chunk.deprecated
                       select chunk).ToList();

            foreach (var chunk in deprecatedChunks)
            {
                if (chunk.blockCount != 0) Level.GetInstance().SaveChunk(chunk);
                RemoveChunk(chunk);
                Destroy(chunk.gameObject);
            }


        }
    }

    public void PlaceBlock(BlockID id, Vector3Int p)
    {
        PlaceBlock(id, p.x, p.y, p.z);
    }

    // Invoke when player place block
    public void PlaceBlock(BlockID id, int x, int y, int z)
    {
        var chunkPos = World2ChunkPos(x, y, z);

        if (GetChunkAt(chunkPos) is Chunk chunk)
        {
            //Debug.Log($"Found chunk at: {chunkPos}");
            chunk.PlaceBlock(id, x - (int)chunkPos.x, y - (int)chunkPos.y, z - (int)chunkPos.z);
        }
    }

    public void SetBlock(BlockID id, int x, int y, int z)
    {
        var chunkPos = World2ChunkPos(x, y, z);
        //Debug.Log($"World to chunk position: {new Vector3(x, y, z)} -> {chunkPos}");
        if (GetChunkAt(chunkPos) is Chunk chunk)
        {
            //Debug.Log($"Found chunk at: {chunkPos}");
            chunk.SetBlock(id, x - (int)chunkPos.x, y - (int)chunkPos.y, z - (int)chunkPos.z);
        }
    }

    public BlockID GetBlock(Vector3Int p)
    {
        return GetBlock(p.x, p.y, p.z);
    }

    public BlockID GetBlock(int x, int y, int z)
    {
        var chunkPos = World2ChunkPos(x, y, z);
        if (GetChunkAt(chunkPos) is Chunk chunk)
        {
            return chunk.Data[x - (int)chunkPos.x, y - (int)chunkPos.y, z - (int)chunkPos.z];
        }
        return BlockID.Air;
    }

    public static Vector3 World2ChunkPos(float x, float y, float z)
    {
        return 16.0f * new Vector3(Mathf.Floor(x / 16), Mathf.Floor(y / 16), Mathf.Floor(z / 16));
    }

    public static Vector3 World2ChunkPos(Vector3 p)
    {
        return World2ChunkPos(p.x, p.y, p.z);
    }

    private Chunk GetChunkAt(Vector3 chunkPos)
    {
        foreach (var chunk in chunks)
        {
            if (Mathf.Approximately(Vector3.Distance(chunkPos, chunk.transform.position), 0))
            {
                return chunk;
            }
        }

        return null;
    }

    private void RemoveChunk(Chunk c)
    {
        c.ClearNeighbours();
        chunks.Remove(c);
    }

    private Chunk AddChunk(Vector3 position)
    {
        Chunk newChunk = Instantiate(chunkPrefab, position, Quaternion.identity, transform);
        newChunk.deprecated = false;

        newChunk.LoadData(Level.GetInstance().GetChunkData(position));

        AddChunk(newChunk);

        return newChunk;
    }

    private void AddChunk(Chunk newChunk)
    {
        foreach (var chunk in chunks)
        {
            if (Mathf.Approximately(Vector3.Distance(chunk.transform.position, newChunk.transform.position), Chunk.CHUNK_SIZE))
            {
                var dir = chunk.transform.position - newChunk.transform.position;
                if (Vector3.Dot(dir, Vector3.left) > 0)
                {
                    chunk.neighbours[ChunkDirection.Right] = newChunk;
                    newChunk.neighbours[ChunkDirection.Left] = chunk;
                }
                else if (Vector3.Dot(dir, Vector3.right) > 0)
                {
                    chunk.neighbours[ChunkDirection.Left] = newChunk;
                    newChunk.neighbours[ChunkDirection.Right] = chunk;
                }
                else if (Vector3.Dot(dir, Vector3.up) > 0)
                {
                    chunk.neighbours[ChunkDirection.Bottom] = newChunk;
                    newChunk.neighbours[ChunkDirection.Top] = chunk;
                }
                else if (Vector3.Dot(dir, Vector3.down) > 0)
                {
                    chunk.neighbours[ChunkDirection.Top] = newChunk;
                    newChunk.neighbours[ChunkDirection.Bottom] = chunk;
                }
                else if (Vector3.Dot(dir, Vector3.forward) > 0)
                {
                    chunk.neighbours[ChunkDirection.Near] = newChunk;
                    newChunk.neighbours[ChunkDirection.Far] = chunk;
                }
                else
                {
                    chunk.neighbours[ChunkDirection.Far] = newChunk;
                    newChunk.neighbours[ChunkDirection.Near] = chunk;
                }
            }
        }

        chunks.Add(newChunk);
    }
}
